package deng.service;

import java.util.Arrays;

import org.apache.log4j.Logger;

import deng.service.util.Utils;

/**
 * Start a {@link Service} class and wait for shutdown event (e.g. CTRL+C). User must provide
 * service with a default constructor.
 * 
 * User may use System property 'waitAndNotify' to false to exit without wait and blocking
 * thread.
 * 
 * If the target service implements {@link Initable#init(String[])}, then it will be called 
 * with all the command line arguments from this main, except the first element(which is its 
 * class name).
 * 
 * Example Usage:
 *   java deng.service.ServiceRunner deng.service.SimpleService
 *   or
 *   java -DwaitAndNotify=false deng.service.ServiceRunner deng.service.SimpleService
 *
 * @author Zemian Deng
 */
public final class ServiceRunner implements Service, Initable {
	
	private static Logger logger = Logger.getLogger(ServiceRunner.class);
	private Service targetService;
	private boolean waitAndNotify;
	
	public void setTargetService(Service targetService) {
		this.targetService = targetService;
	}
	public void setWaitAndNotify(boolean waitAndNotify) {
		this.waitAndNotify = waitAndNotify;
	}
	
	public void run() {
		logger.debug("Running");
		start();
		if (waitAndNotify) {
			registerShutdownEvent();
			waitForShutdownEvent();
		} else {
			stop();
		}
	}

	@Override
	public void init(String[] args) {
		logger.debug("Init " + Utils.arrayToString("args", args));
		if (args.length < 1) {
			throw new RuntimeException("Usage: " + ServiceRunner.class.getName() + " <serviceClassName> [serviceArguments]");
		}

		// Set targetService from args
		String serviceClassName = args[0];
		String[] serviceArgs = Arrays.copyOfRange(args, 1, args.length);
		
		try {
			// Load service class
			logger.debug("Loading serviceClassName " + serviceClassName);
			Class<?> cls = Class.forName(serviceClassName);
			if (!Service.class.isAssignableFrom(cls))
				throw new RuntimeException("Class " + serviceClassName + " did not implements interface: " + Service.class.getName());
			
			logger.debug("Creating new targetService instance with default constructor.");
			targetService = (Service)cls.newInstance();
				
			// Check for Initable implementation
			if (Initable.class.isAssignableFrom(cls)) {
				logger.debug("Calling targetService#init(String[]) with " + Utils.arrayToString("serviceArgs", serviceArgs));
				((Initable)targetService).init(serviceArgs);
			}
		} catch (Exception e) {
			throw new RuntimeException("Failed to create service: " + serviceClassName, e);
		}
		
		// Set waitAndNotify from sys props.
		String flag = System.getProperty("waitAndNotify", "true");
		logger.debug("Setting waitAndNotify=" + flag);
		setWaitAndNotify(Boolean.valueOf(flag));
	}

	@Override
	public void start() {
		targetService.start();
	}

	@Override	
	public void stop() {
		targetService.stop();
	}

	private void registerShutdownEvent() {
		logger.debug("Registering a callback thread with Runtime#shutdownHook.");
		Runtime.getRuntime().addShutdownHook(new Thread() {
			@Override
			public void run() {
				logger.debug("Shutdown thread started.");
				
				// Stop this service.
				ServiceRunner.this.stop();
				
				// Notify event.
				if (waitAndNotify) 
					notifyShutdownEvent();
			}
		});
	}

	private void waitForShutdownEvent() {
		logger.debug("Wait and block current thread.");
		synchronized(this) {
			try {
				this.wait();
			} catch (InterruptedException e) {
				throw new RuntimeException(e);
			}
		}		
	}

	private void notifyShutdownEvent() {
		logger.debug("Wait and block current thread.");
		synchronized(this) {
			this.notifyAll();
		}
	}

	public static void main(final String[] args) {
		ServiceRunner runner = new ServiceRunner();
		runner.init(args);
		runner.run();
	}
}
